환경변수·시그널·에러 처리 실습에서 errno 때문에 생긴 버그를 모아, 언제 0으로 만들고, 언제 읽고, 언제 저장하고, 언제 설정하는지를 한곳에 정리한 규칙 노트다. 실습 코드는 Docker Desktop의 Linux 컨테이너(c_app_part1)에서 gcc로 확인했다.
errno는 “마지막으로 실패한 함수의 이유”를 담는 **스레드별 int**다. 믿을 수 있는 순간이 짧아서 아래 규칙으로만 쓴다.
flowchart TD
A["함수 호출"] --> B{"반환값이 실패인가?"}
B -- "아니오" --> C["errno는 읽지 않는다<br/>성공해도 값이 바뀔 수 있음"]
B -- "예" --> D{"읽기 전에 printf·로그·free·close를 부르는가?"}
D -- "아니오" --> E["errno를 바로 읽는다<br/>perror · strerror"]
D -- "예" --> F["int saved = errno;<br/>먼저 저장 후 정리"]
F --> G["필요하면 errno = saved<br/>호출한 쪽에 전달"]
H["strtol 계열처럼<br/>반환값만으로 구분 불가"] -.-> I["호출 직전에 errno = 0"]
I -.-> A
#
규칙
이유
1
실패는 반환값으로 먼저 판단하고, 그 뒤에 errno를 읽는다
errno는 성공한 호출도 바꿀 수 있어서, errno != 0만으로는 실패를 알 수 없다
2
errno로만 실패를 알리는 함수는 호출 직전에 errno = 0
자동으로 0이 되지 않아 이전 값이 남는다
3
읽기 직후가 아니면 저장한다 (int saved = errno;)
로그·free·fclose·close가 값을 덮을 수 있다
4
시그널 핸들러는 errno를 저장하고 복원한다
핸들러 안의 write 등이 main이 보던 값을 바꾼다
5
우리가 만드는 함수는 실패할 때만errno를 설정한다
성공 시 errno = 0으로 지우면 호출한 쪽의 이전 값을 망가뜨린다
6
스트림(printf 등)의 오류는 errno가 아니라 ferror
호출이 여러 개면 errno가 어느 호출 것인지 알 수 없다
1. 초기화 — errno = 0이 필요한 경우 (규칙 2)
요약
문제: strtol처럼 반환값만으로 실패를 구분 못 하는 함수에서 이전 ERANGE가 남아 오판 (13-06)
원인: errno는 성공해도 지워지지 않고, 프로그램 시작 때만 0
처방: 그런 함수는 호출 직전에 errno = 0. 반환값(-1/NULL)으로 구분되는 함수는 반환값만 먼저 본다
상세 보기
errno는 프로그램 시작 때 0이지만 그 뒤로는 라이브러리가 실패할 때만 바꾸고, 성공해도 지우지 않는다. 반환값으로 실패를 구분할 수 없는 함수는 호출 전에 직접 0으로 만든다.
errno = 0;val = strtol(buf, &end, 10); // 반환값 LONG_MAX/LONG_MIN 이 진짜 값인지 오류인지 구분 불가if (errno == ERANGE) { ... } // errno 로만 구분
해당 함수: strtol, strtoul, strtod 계열, readdir의 EOF/오류 구분, fgetc 등을 errno로 구분하는 경우.
strtoul을 errno = 0 없이 쓰면 이전에 남은 ERANGE 를 이번 실패로 오판한다 (13-06).
대부분의 함수는 -1/NULL 같은 반환값으로 실패를 알린다. 그 경우엔 초기화가 필요 없고 반환값을 먼저 본다.
fd = open(path, O_RDONLY);if (fd < 0) { /* 여기서만 errno 가 의미 있다 */ perror("open"); }
함수 종류별로 errno를 쓰는 법 (교육자료 13-6)
함수 종류별 표 (out-of-band / in-band / 미보장 / 표준 차이)
함수 종류
예
errno = 0 초기화
errno를 읽는 시점
errno를 설정하고 out-of-band 값(NULL, -1 등 정상 결과와 구분되는 값)을 반환
open, malloc(POSIX)
불필요
반환값이 에러일 때만
errno를 설정하고 in-band 값(에러 값이 정상 결과로도 나올 수 있음)을 반환
strtol, strtoul (LONG_MAX가 진짜 값일 수도 있음)
필수
호출 직후, 반환값만으로 모호할 때
errno 설정을 보장하지 않거나 문서화하지 않음
일부 C 표준 함수
—
읽지 않는다. 실패 여부는 반환값으로만
표준에 따라 동작이 다름
fopen — C 표준은 실패 시 NULL만 말하고, POSIX는 errno도 설정한다고 명시
—
POSIX 환경이라고 확실할 때만 읽는다 (이식성이 필요하면 의존하지 않는다)
NOTE
“errno를 0으로 만들 필요가 있는 함수”는 반환값으로 에러를 구분할 수 없는 함수라는 한 줄로 외우면 된다. 반환값으로 구분되는 함수 앞에서 errno = 0을 습관처럼 넣는 것은 해롭지는 않지만, 규칙을 흐리게 만든다.
2. 검사 — 판단은 반환값, 이유는 errno (규칙 1, 6)
요약
문제: printf 여러 번 뒤 errno != 0 검사, if (errno = ERANGE) 대입 오타, 이유를 안 읽는 로그
원인: 성공한 호출도 errno를 바꾸고, =는 대입이라 항상 참
처방: 반환값 확인 → 그 뒤 errno == 비교, 스트림은 ferror, 이유는 perror/strerror
상세 보기
하지 말 것
이유
대신
printf 여러 번 → if (errno != 0) (13-01)
성공한 호출도 errno를 바꿀 수 있고, 어느 호출의 것인지 모른다
ferror(stdout)
if (errno = ERANGE && ...)
=는 대입이라 항상 참이 되고 errno를 덮어쓴다 (my_atoi 처음 코드)
errno == ERANGE
if (errno) 만으로 실패 판단
이전 호출의 값이 남아 있을 수 있다
반환값 확인 → 그 뒤 errno
이유를 안 읽고 fdopen 실패를 printf("fdopen failed")
원인이 가려진다
perror("fdopen") / strerror(errno)
3. 저장 — 읽기 전에 덮어쓰이는 경우 (규칙 3, 4)
정리 코드와 로그 사이에서
요약
문제: 실패 후 로그·free·fclose·close가 errno를 덮어씀
원인: 정리 함수들이 내부에서 errno를 바꿀 수 있음
처방: 실패 직후 int saved = errno;, 필요하면 errno = saved로 복원 (LOG_ERRNO가 같은 방식)
상세 보기
fp = fopen(path, "r");if (fp == NULL) { int saved = errno; // 먼저 저장 LOG_ERR("open fail"); // 로그 안의 time()/localtime_r() 이 errno 를 바꿀 수 있다 SAFE_FREE(buf); // free 도 errno 를 바꿀 수 있다 errno = saved; // 필요하면 복원해서 호출한 쪽에 전달 return RET_IO_ERROR;}
goto CLEANUP으로 free/fclose/close를 부르는 구간에서 errno를 쓰려면, 실패 직후에 ret이나 지역 변수로 먼저 저장한다 (동적 메모리 노트의 errno 먼저 저장).
log.h의 LOG_ERRNO는 이 규칙을 매크로로 만든 것이다. int _e = errno;를 먼저 저장한 뒤 strerror(_e)로 출력한다.
시그널 핸들러 (규칙 4)
요약
문제: 핸들러 안의 write 등이 main이 읽으려던 errno를 바꿈
원인: 핸들러는 어느 지점에서든 끼어듦
처방: 핸들러 시작에서 저장, 끝에서 복원. 가장 안전한 것은 flag만 세우기
상세 보기
static void skip_hdlr(int signo){ int saved = errno; // main 이 읽던 errno 보호 ssize_t r = write(STDOUT_FILENO, "skip signal\n", 12); (void)r; (void)signo; errno = saved; // 복원}
핸들러는 어느 지점에서든 끼어든다. 그 안에서 errno를 바꾸는 함수를 부르면, main이 직전에 실패 이유를 읽으려던 errno가 바뀐다.
가장 안전한 핸들러는 flag만 세우는 것이다. write 등을 부를 때만 저장·복원한다.
4. 설정 — 우리가 만드는 함수가 errno로 알릴 때 (규칙 5)
요약
문제: NULL/-1로만 알리는 우리 함수는 실패 이유를 전달할 곳이 없음
원인: 반환값에 이유를 담을 수 없음
처방: 실패할 때만 errno 설정 (EINVAL/ERANGE/…), 성공 시 errno = 0으로 지우지 않음. 에러 코드를 반환값으로 돌려주는 방식도 가능
상세 보기
반환값이 NULL/-1이라 이유를 담을 곳이 없으면 errno에 실패한 경우에만 값을 넣는다.